﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.VisualStudio.VCCodeModel;
using EnvDTE;
using Microsoft.VisualStudio.Shell;
using System.Text.RegularExpressions;
using Utility.ModifyRegistry;
using Microsoft.Win32;

namespace GordonKristan.codeclean
{
	public partial class CommentManager : Form
	{
		private String document;
		private int currentIndex;
		private ArrayList chunks;
		private ModifyRegistry reg;
		public bool success;

		/// <summary>
		/// Default constructor for dialog.
		/// </summary>
		/// <param name="aDoc">String containing file text</param>
		public CommentManager(String aDoc)
		{
			InitializeComponent();
			//Handle keyboard presses
			this.KeyPreview = true;
			this.KeyPress += new KeyPressEventHandler(CommentManager_keyPress);
			//Registry settings
			reg = new ModifyRegistry();
			reg.BaseRegistryKey = Registry.CurrentUser;
			reg.SubKey = "SOFTWARE\\CODECLEAN";
			getRegValues();
			//Have our margin adjust to the number of lines in the file
			String count = String.Format("{0}", linesInString(aDoc));
			this.previewBox.Margins.Margin0.Width = 10 * count.Length;
			//Set up our basic variables
			success = false;
			document = aDoc;
			chunks = new ArrayList();
			//Insert the data into the dialog
			insertAllComments();
		}

		#region Helper_Function
		/// <summary>
		/// Reads registry values and assigns them accordingly.
		/// </summary>
		private void getRegValues()
		{
			String width = reg.Read("COMMENTMANAGERWIDTH");
			String height = reg.Read("COMMENTMANAGERHEIGHT");
			if (width == null)
			{
				this.Width = 800;
			}
			else
			{
				this.Width = int.Parse(width);
			}
			if (height == null)
			{
				this.Height = 600;
			}
			else
			{
				this.Height = int.Parse(height);
			}
		}

		/// <summary>
		/// Saves registry values like dialog size.
		/// </summary>
		private void saveRegValues()
		{
			String width = String.Format("{0}", this.Width);
			String height = String.Format("{0}", this.Height);
			reg.Write("COMMENTMANAGERWIDTH", width);
			reg.Write("COMMENTMANAGERHEIGHT", height);
		}

		/// <summary>
		/// Updates the document with the current chunks
		/// then update the preview box.
		/// </summary>
		private void updateDocument()
		{
			//Update preview box
			previewBox.IsReadOnly = false;
			document = "";
			for (int i = 0; i < chunks.Count; ++i)
			{
				document += ((StringContainer)chunks[i]).newText;
			}
			previewBox.Text = document;
			previewBox.IsReadOnly = true;
		}

		/// <summary>
		/// Retrieves the text associated with this dialog.
		/// </summary>
		/// <returns>String of document</returns>
		public String getText()
		{
			return document;
		}

		/// <summary>
		/// Parses all of the chunks in the current document,
		/// and inserts them into the list box.
		/// </summary>
		private void insertAllComments()
		{
			CommentEditModule mod = new CommentEditModule(document);
			chunks = mod.getComments();
			sortComments(false);
			updateDocument();
			for (int i = 0; i < chunks.Count; ++i)
			{
				if (((StringContainer)chunks[i]).comment)
				{
					selectComment(i);
					break;
				}
			}
		}

		/// <summary>
		/// Takes the current comment and updates it with
		/// the new content input by the user.
		/// </summary>
		private void saveCurrentComment()
		{
			StringContainer cur = ((StringContainer)chunks[currentIndex]);
			if(oldContentBox.Text.EndsWith("\r\n") && !newContentBox.Text.EndsWith("\r\n"))
			{
				newContentBox.Text += "\r\n";
			}
			int diff = newContentBox.Text.Length - cur.newText.Length;
			document = replaceSubstring(document, cur.start, cur.length, newContentBox.Text);
			cur.newText = newContentBox.Text;
			cur.length = newContentBox.Text.Length;
			for (int i = currentIndex + 1; i < chunks.Count; ++i)
			{
				((StringContainer)chunks[i]).start += diff;
			}
			previewBox.IsReadOnly = false;
			previewBox.Text = document;
			previewBox.IsReadOnly = true;
		}

		/// <summary>
		/// Selects a comment by putting its info in the
		/// boxes and highlighting it in the preview box.
		/// </summary>
		/// <param name="index"></param>
		private void selectComment(int index)
		{
			//Set the content boxes to the correct text
			oldContentBox.IsReadOnly = false;
			oldContentBox.Text = ((StringContainer)chunks[index]).oldText;
			oldContentBox.IsReadOnly = true;
			newContentBox.Text = ((StringContainer)chunks[index]).newText;
			//Move the caret and select the old comment in the preview box
			previewBox.CurrentPos = ((StringContainer)chunks[index]).start;
			previewBox.Selection.Start = ((StringContainer)chunks[index]).start;
			previewBox.Selection.End = ((StringContainer)chunks[index]).start +
				((StringContainer)chunks[index]).length;
			//Mark our current comment
			currentIndex = index;
		}

		/// <summary>
		/// Gets the index of a comment in the arraylist
		/// based on a position in the text string.
		/// </summary>
		/// <param name="pos">Position in string</param>
		/// <returns>Index in array</returns>
		private int getCommentIndex(int pos)
		{
			for (int i = 0; i < chunks.Count; ++i)
			{
				StringContainer c = (StringContainer)chunks[i];
				if (pos >= c.start && pos <= c.start + c.length - 1 && c.comment)
				{
					return i;
				}
			}
			return -1;
		}

		/// <summary>
		/// Sorts the ArrayList of chunks.
		/// </summary>
		/// <param name="reverse">True for reverse sort, else false</param>
		private void sortComments(bool reverse)
		{
			IComparer sorter = new StringContainerSorter(reverse);
			chunks.Sort(sorter);
		}

		/// <summary>
		/// Replaces a portion of string with a new string.
		/// Use -1 as length to replace rest of string from start onwards.
		/// </summary>
		/// <param name="aDoc">String to perform replacement on</param>
		/// <param name="start">Start of substring to replace</param>
		/// <param name="length">Length of substring to replace</param>
		/// <param name="replace">String to replace with</param>
		private String replaceSubstring(String aDoc, int start, int length, String replace)
		{
			if (length == 0)
			{
				return aDoc;
			}
			String begin, end;
			if (start == 0)
			{
				begin = "";
			}
			else
			{
				begin = aDoc.Substring(0, start);
			}
			if (length == -1)
			{
				end = "";
			}
			else
			{
				end = aDoc.Substring(start + length);
			}
			return begin + replace + end;
		}

		/// <summary>
		/// Returns the number of lines in a string.
		/// </summary>
		/// <param name="aDoc">String to count lines in</param>
		/// <returns>Number of lines in the string</returns>
		private int linesInString(String aDoc)
		{
			int count = 1;
			int start = 0;
			for (;;)
			{
				start = aDoc.IndexOf("\r\n", start);
				if (start != -1)
				{
					count++;
					start += 2;
				}
				else
				{
					break;
				}
			}
			return count;
		}
		#endregion

		#region Dialog_Events
		/// <summary>
		/// Called when the user saves their changes.
		/// </summary>
		private void saveButton_Click(object sender, EventArgs e)
		{
			saveCurrentComment();
			success = true;
			saveRegValues();
			Close();
		}

		/// <summary>
		/// Called when the user discards their changes.
		/// </summary>
		private void cancelButton_Click(object sender, EventArgs e)
		{
			success = false;
			saveRegValues();
			Close();
		}

		/// <summary>
		/// Called when the user deletes a comment.
		/// </summary>
		private void deleteButton_Click(object sender, EventArgs e)
		{
			newContentBox.Text = "";
			nextButton.PerformClick();
		}

		/// <summary>
		/// Called when the user chooses the previous comment.
		/// </summary>
		private void prevButton_Click(object sender, EventArgs e)
		{
			saveCurrentComment();
			for (int i = currentIndex - 1; i >= 0; i--)
			{
				if (((StringContainer)chunks[i]).comment)
				{
					selectComment(i);
					return;
				}
			}
			for (int i = chunks.Count - 1; i >= currentIndex; i--)
			{
				if (((StringContainer)chunks[i]).comment)
				{
					selectComment(i);
					return;
				}
			}
		}

		/// <summary>
		/// Called when the user chooses the next comment.
		/// </summary>
		private void nextComment_Click(object sender, EventArgs e)
		{
			saveCurrentComment();
			for (int i = currentIndex + 1; i < chunks.Count; i++)
			{
				if (((StringContainer)chunks[i]).comment)
				{
					selectComment(i);
					return;
				}
			}
			for (int i = 0; i <= currentIndex; i++)
			{
				if (((StringContainer)chunks[i]).comment)
				{
					selectComment(i);
					return;
				}
			}
		}

		/// <summary>
		/// Called when the user reverts a comment.
		/// </summary>
		private void resetButton_Click(object sender, EventArgs e)
		{
			newContentBox.Text = oldContentBox.Text;
		}

		/// <summary>
		/// Called when the user clicks on the file preview box.
		/// </summary>
		private void fileBox_Click(object sender, EventArgs e)
		{
			saveCurrentComment();
			int commentPos = getCommentIndex(previewBox.CurrentPos);
			if (commentPos < 0 || commentPos >= chunks.Count)
			{
				return;
			}
			selectComment(commentPos);
		}

		/// <summary>
		/// Called on resize to resize controls.
		/// </summary>
		private void CommentManager_Resize(object sender, System.EventArgs e)
		{
			Point p;
			//Next Button
			p = new Point(nextButton.Location.X, prevButton.Location.Y);
			nextButton.Location = p;
			//Revert Comment Button
			p = new Point(resetButton.Location.X, deleteButton.Location.Y);
			resetButton.Location = p;
			//New Content Label
			p = new Point(newContentLabel.Location.X, originalContentLabel.Location.Y);
			newContentLabel.Location = p;
			//Discard Changes Button
			p = new Point(saveButton.Location.X - saveButton.Margin.Left -
				cancelButton.Margin.Right - cancelButton.Width, cancelButton.Location.Y);
			cancelButton.Location = p;
			//Expand File Preview Box
			previewBox.Height = prevButton.Location.Y - prevButton.Margin.Top -
				previewBox.Margin.Bottom - previewBox.Top;
			//Expand old content box
			oldContentBox.Width = newContentLabel.Left - newContentLabel.Margin.Left -
				oldContentBox.Margin.Right - oldContentBox.Left;
			//Expand new content box
			p = new Point(newContentLabel.Left, newContentBox.Top);
			newContentBox.Location = p;
			newContentBox.Width = previewBox.Right - newContentBox.Left;
		}

		/// <summary>
		/// Called when the user presses a keyboard button.
		/// </summary>
		private void CommentManager_keyPress(Object o, KeyPressEventArgs e)
		{
			//If they're typing a new comment, don't interrupt
			if (newContentBox.Focused)
			{
				return;
			}
			//Otherwise, handle the keys
			switch (e.KeyChar)
			{
				case 'n':
					nextButton.PerformClick();
					break;
				case 'p':
					prevButton.PerformClick();
					break;
				case 'r':
					resetButton.PerformClick();
					break;
				case 'd':
					deleteButton.PerformClick();
					break;
			}
		}

		#endregion

		#region Add_Comment_Code
		/// <summary>
		/// Called when the user double-clicks the file preview box.
		/// </summary>
		private void fileBox_DoubleClick(object sender, EventArgs e)
		{
			saveCurrentComment();
			//Check if they clicked on a comment
			int commentPos = getCommentIndex(previewBox.CurrentPos);
			if (commentPos >= 0 && commentPos < chunks.Count)
			{
				selectComment(commentPos);
				return;
			}
			/*//If not, check if they clicked on a function
			VCCodeFunction f = getFunction(previewBox.CurrentPos);
			if (f == null)
			{
				return;
			}
			//If they did, make sure the function has no comment
			if (f.Comment.Length > 0)
			{
				return;
			}
			//Otherwise, insert the comment
			addComment(f.StartPoint.AbsoluteCharOffset + f.StartPoint.Line - 1);*/
		}

		/// <summary>
		/// Inserts a new comment at the specified position
		/// </summary>
		/// <param name="pos">Position to add new comment</param>
		private void addComment(int pos)
		{
			//The comment to insert.
			String newComment = "/* Automatically Generated Comment */\r\n";
			//Find chunk with start position in it
			int i;
			for (i = 0; i < chunks.Count; ++i)
			{
				StringContainer s = (StringContainer)chunks[i];
				if (pos >= s.start && pos <= s.start + s.length - 1)
				{
					break;
				}
			}
			StringContainer split = (StringContainer)chunks[i];
			chunks.RemoveAt(i);
			pos -= (split.start + 1);
			split.oldText = split.oldText.Insert(pos, newComment);
			CommentEditModule mod = new CommentEditModule(split.oldText);
			ArrayList newChunks = mod.getComments();
			foreach (StringContainer ns in newChunks)
			{
				ns.start += split.start;
				chunks.Add(ns);
			}
			foreach (StringContainer cs in chunks)
			{
				if (cs.start > split.start)
				{
					cs.start += newComment.Length;
				}
			}
			sortComments(false);
			updateDocument();
		}

		/// <summary>
		/// Gets the index of a function in the code model
		/// function list based on position in the text
		/// </summary>
		/// <param name="pos">Position in the string</param>
		/// <returns>Index in function array</returns>
		private VCCodeFunction getFunction(int pos)
		{
			//Get the IDE object
			EnvDTE.DTE dte = (EnvDTE.DTE)Package.GetGlobalService(typeof(EnvDTE.DTE));
			if (dte == null)
			{
				return null;
			}
			//Get the code model for this file
			VCFileCodeModel file = (VCFileCodeModel)dte.ActiveDocument.ProjectItem.FileCodeModel;
			if (file == null)
			{
				return null;
			}
			//Iterate through all of the functions
			foreach (VCCodeFunction f in file.Functions)
			{
				//Ensure that we only deal with functions in THIS file
				if (String.Compare(f.File, dte.ActiveDocument.FullName, true) == 0)
				{
					//1 based index, not 0
					int start = f.StartPoint.AbsoluteCharOffset + f.StartPoint.Line - 2;
					int end = f.EndPoint.AbsoluteCharOffset + f.EndPoint.Line - 1;
					//If our cursor was in this function, return its index
					if (pos >= start && pos <= end)
					{
						return f;
					}
				}
			}
			return null;
		}
		#endregion
	}
}
